/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2008-2011, Salzburg Research. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * - Neither the name of the KiWi Project nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * Contributor(s):
 * Stefan
 *
 * File Info:
 * User: Stefan
 * Date: 2011/01/26
 * Time: 11:58
 *
 * Project: KiWi2, kiwi-core module
 */

package kiwi.core.services.io;

import org.openrdf.model.*;
import org.openrdf.model.vocabulary.RDF;
import org.openrdf.model.vocabulary.RDFS;
import org.openrdf.rio.RDFHandlerException;
import org.openrdf.rio.rdfxml.util.RDFXMLPrettyWriter;

import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.util.Stack;


/**
 * RdfQuadsWriter is a modified version of the RDFXMLPrettyWriter.java. It has support for multiple graphs in one file.
 * RdfQuadsWriter based on "RDFa Quads". But it owns the graph attribute a namespace and a graph definition of a
 * subject define the graph of his outgoing triples, but a triple can override (if exist) the definition of his subject.
 *
 * Created by IntelliJ IDEA.
 * User: Stefan
 * Date: 26.01.11
 * Time: 11:58
 * To change this template use File | Settings | File Templates.
 */
public class RdfQuadsWriter extends RDFXMLPrettyWriter {

    /*
      * We implement striped syntax by using two stacks, one for predicates and
      * one for subjects/objects.
      */

    /**
     * Stack for remembering the nodes (subjects/objects) of statements at each
     * level.
     */
    private Stack<Node> nodeStack = new Stack<Node>();

    /**
     * Stack for remembering the predicate of statements at each level.
     */
    private Stack<URI> predicateStack = new Stack<URI>();

    /**
     * uri for the namespace of the new attribute
     */
    private final static String GRAPH_EXTENSION_NAMESPACE = "http://www.kiwi-community.eu/rdfquads#";

    /**
     * Creates a new RDFXMLPrintWriter that will write to the supplied
     * OutputStream.
     *
     * @param out The OutputStream to write the RDF/XML document to.
     */
    public RdfQuadsWriter(OutputStream out) {
        super(out);
    }

    /**
     * Creates a new RDFXMLPrintWriter that will write to the supplied Writer.
     *
     * @param out The Writer to write the RDF/XML document to.
     */
    public RdfQuadsWriter(Writer out) {
        super(out);
    }

    @Override
    protected void writeHeader()
            throws IOException {

        setNamespace("rdfs", RDFS.NAMESPACE);
        //TODO: build rdfs file for graphextension
        setNamespace("graph", GRAPH_EXTENSION_NAMESPACE);

        super.writeHeader();
    }

    public void flush()
            throws IOException {
        if (writingStarted) {
            if (!headerWritten) {
                writeHeader();
            }

            try {
                flushPendingStatements();
            } catch (RDFHandlerException e) {
                e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
            }
            writer.flush();
        }
    }

    protected void writeStartTag(String namespace, String localName, Value graph)
            throws IOException, RDFHandlerException {
        writeStartOfStartTag(namespace, localName);
        writeNamedGraphAdd(graph);
        writeEndOfStartTag();
    }


    public void close()
            throws IOException {
        try {
            if (writingStarted) {
                endRDF();
            }
        } catch (RDFHandlerException e) {
            if (e.getCause() instanceof IOException) {
                throw (IOException) e.getCause();
            } else {
                IOException ioe = new IOException(e.getMessage());
                ioe.initCause(e);
                throw ioe;
            }
        } finally {
            writer.close();
        }
    }

    @Override
    protected void flushPendingStatements()
            throws IOException, RDFHandlerException {
        if (!nodeStack.isEmpty()) {
            popStacks(null);
        }
    }

    /**
     * Write out the stacks until we find subject. If subject == null, write out
     * the entire stack
     *
     * @param newSubject
     */
    private void popStacks(Resource newSubject)
            throws IOException, RDFHandlerException {
        // Write start tags for the part of the stacks that are not yet
        // written
        for (int i = 0; i < nodeStack.size() - 1; i++) {
            Node node = nodeStack.get(i);

            if (!node.isWritten()) {
                if (i > 0) {
                    writeIndents(i * 2 - 1);

                    URI predicate = predicateStack.get(i - 1);

                    writeStartTag(predicate.getNamespace(), predicate.getLocalName(),node.getGraphOfTriple() );
                    writeNewLine();
                }

                writeIndents(i * 2);
                writeNodeStartTag(node);
                node.setIsWritten(true);
            }
        }

        // Write tags for the top subject
        Node topNode = nodeStack.pop();

        if (predicateStack.isEmpty()) {
            // write out an empty subject
            writeIndents(nodeStack.size() * 2);
            writeNodeEmptyTag(topNode);
            writeNewLine();
        } else {
            URI topPredicate = predicateStack.pop();

            writeIndents(nodeStack.size() * 2 - 1);
            writeAbbreviatedPredicate(topPredicate, topNode.getValue(), topNode.getGraphOfTriple());
        }

        // Write out the end tags until we find the subject
        while (!nodeStack.isEmpty()) {
            Node nextElement = nodeStack.peek();

            if (nextElement.getValue().equals(newSubject)) {
                break;
            } else {
                nodeStack.pop();

                // We have already written out the subject/object,
                // but we still need to close the tag
                writeIndents(predicateStack.size() + nodeStack.size());

                writeNodeEndTag(nextElement);

                if (predicateStack.size() > 0) {
                    URI nextPredicate = predicateStack.pop();

                    writeIndents(predicateStack.size() + nodeStack.size());

                    writeEndTag(nextPredicate.getNamespace(), nextPredicate.getLocalName());

                    writeNewLine();
                }
            }
        }
    }

    @Override
    public void handleStatement(Statement st)
            throws RDFHandlerException {
        if (!writingStarted) {
            throw new RuntimeException("Document writing has not yet been started");
        }

        Resource subj = st.getSubject();
        URI pred = st.getPredicate();
        Value obj = st.getObject();
        Resource cont = st.getContext();

        try {
            if (!headerWritten) {
                writeHeader();
            }

            if (!nodeStack.isEmpty() && !subj.equals(nodeStack.peek().getValue())) {
                // Different subject than we had before, empty the stack
                // until we find it
                popStacks(subj);
            }

            // Stack is either empty or contains the same subject at top

            if (nodeStack.isEmpty()) {
                // Push subject
                nodeStack.push(new Node(subj));
            }

            // Stack now contains at least one element
            Node topSubject = nodeStack.peek();

            // Push predicate and object
            predicateStack.push(pred);
            Node newNode = new Node(obj);
            newNode.setGraphOfTriple((URI) cont);
            nodeStack.push(newNode);

        } catch (IOException e) {
            throw new RDFHandlerException(e);
        }
    }

    /**
     * Write out the opening tag of the subject or object of a statement up to
     * (but not including) the end of the tag. Used both in writeStartSubject and
     * writeEmptySubject.
     */
    private void writeNodeStartOfStartTag(Node node)
            throws IOException, RDFHandlerException {
        Value value = node.getValue();

        writeStartOfStartTag(RDF.NAMESPACE, "Description");

        if (value instanceof URI) {
            URI uri = (URI) value;
            writeAttribute(RDF.NAMESPACE, "about", uri.toString());
        } else {
            BNode bNode = (BNode) value;
            writeAttribute(RDF.NAMESPACE, "nodeID", bNode.getID());
        }
    }

    private void writeNamedGraphAdd(Value value) throws IOException, RDFHandlerException {

        if(value != null && value instanceof URI) {
            URI uri = (URI) value;
            //TODO: this attribute need a namespace
            writeAttribute(GRAPH_EXTENSION_NAMESPACE, "graph", uri.toString());
        }
    }

    /**
     * Write out the opening tag of the subject or object of a statement.
     */
    private void writeNodeStartTag(Node node)
            throws IOException, RDFHandlerException {
        writeNodeStartOfStartTag(node);
        writeEndOfStartTag();
        writeNewLine();
    }

    /**
     * Write out the closing tag for the subject or object of a statement.
     */
    private void writeNodeEndTag(Node node)
            throws IOException {
        writeEndTag(RDF.NAMESPACE, "Description");

        writeNewLine();
    }

    /**
     * Write out an empty tag for the subject or object of a statement.
     */
    private void writeNodeEmptyTag(Node node)
            throws IOException, RDFHandlerException {
        writeNodeStartOfStartTag(node);
        writeEndOfEmptyTag();
    }

    /**
     * Write out an empty property element.
     */
    private void writeAbbreviatedPredicate(URI pred, Value obj, Value cont)
            throws IOException, RDFHandlerException {

        writeStartOfStartTag(pred.getNamespace(), pred.getLocalName());
        if(cont != null) this.writeNamedGraphAdd(cont);

        if (obj instanceof Resource) {
            Resource objRes = (Resource) obj;

            if (objRes instanceof URI) {
                URI uri = (URI) objRes;
                writeAttribute(RDF.NAMESPACE, "resource", uri.toString());
            } else {
                BNode bNode = (BNode) objRes;
                writeAttribute(RDF.NAMESPACE, "nodeID", bNode.getID());
            }

            writeEndOfEmptyTag();
        } else if (obj instanceof Literal) {
            Literal objLit = (Literal) obj;

            // language attribute
            if (objLit.getLanguage() != null) {
                writeAttribute("xml:lang", objLit.getLanguage());
            }

            // datatype attribute
            boolean isXmlLiteral = false;
            URI datatype = objLit.getDatatype();
            if (datatype != null) {
                // Check if datatype is rdf:XMLLiteral
                isXmlLiteral = datatype.equals(RDF.XMLLITERAL);

                if (isXmlLiteral) {
                    writeAttribute(RDF.NAMESPACE, "parseType", "Literal");
                } else {
                    writeAttribute(RDF.NAMESPACE, "datatype", datatype.toString());
                }
            }

            writeEndOfStartTag();

            // label
            if (isXmlLiteral) {
                // Write XML literal as plain XML
                writer.write(objLit.getLabel());
            } else {
                writeCharacterData(objLit.getLabel());
            }

            writeEndTag(pred.getNamespace(), pred.getLocalName());
        }

        writeNewLine();
    }


    /*------------------*
      * Inner class Node *
      *------------------*/

    private static class Node {

        private Value value;

        // type == null means that we use <rdf:Description>
        private URI type = null;

        private boolean isWritten = false;

        private URI graphOfType = null;

        private URI graphOfTriple = null;

        /**
         * Creates a new Node for the supplied Value.
         */
        public Node(Value value) {
            this.value = value;
        }

        public Value getValue() {
            return value;
        }

        public void setType(URI type) {
            this.type = type;
        }

        public void setIsWritten(boolean isWritten) {
            this.isWritten = isWritten;
        }

        public boolean isWritten() {
            return isWritten;
        }

        public URI getGraphOfType() {
            return graphOfType;
        }

        public void setGraphOfType(URI graphOfType) {
            this.graphOfType = graphOfType;
        }

        public URI getGraphOfTriple() {
            return graphOfTriple;
        }

        public void setGraphOfTriple(URI graphOfTriple) {
            this.graphOfTriple = graphOfTriple;
        }
    }


}

